/*
 * Copyright (c) 1996, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package java.util.zip;

import java.io.FilterOutputStream;
import java.io.OutputStream;
import java.io.InputStream;
import java.io.IOException;

/**
 * This class implements an output stream filter for compressing data in
 * the "deflate" compression format. It is also used as the basis for other
 * types of compression filters, such as GZIPOutputStream.
 *
 * @author David Connelly
 * @see Deflater
 */
public class DeflaterOutputStream extends FilterOutputStream {

  /**
   * Compressor for this stream.
   */
  protected Deflater def;

  /**
   * Output buffer for writing compressed data.
   */
  protected byte[] buf;

  /**
   * Indicates that the stream has been closed.
   */

  private boolean closed = false;

  private final boolean syncFlush;

  /**
   * Creates a new output stream with the specified compressor,
   * buffer size and flush mode.
   *
   * @param out the output stream
   * @param def the compressor ("deflater")
   * @param size the output buffer size
   * @param syncFlush if {@code true} the {@link #flush()} method of this instance flushes the
   * compressor with flush mode {@link Deflater#SYNC_FLUSH} before flushing the output stream,
   * otherwise only flushes the output stream
   * @throws IllegalArgumentException if {@code size <= 0}
   * @since 1.7
   */
  public DeflaterOutputStream(OutputStream out,
      Deflater def,
      int size,
      boolean syncFlush) {
    super(out);
    if (out == null || def == null) {
      throw new NullPointerException();
    } else if (size <= 0) {
      throw new IllegalArgumentException("buffer size <= 0");
    }
    this.def = def;
    this.buf = new byte[size];
    this.syncFlush = syncFlush;
  }


  /**
   * Creates a new output stream with the specified compressor and
   * buffer size.
   *
   * <p>The new output stream instance is created as if by invoking
   * the 4-argument constructor DeflaterOutputStream(out, def, size, false).
   *
   * @param out the output stream
   * @param def the compressor ("deflater")
   * @param size the output buffer size
   * @throws IllegalArgumentException if {@code size <= 0}
   */
  public DeflaterOutputStream(OutputStream out, Deflater def, int size) {
    this(out, def, size, false);
  }

  /**
   * Creates a new output stream with the specified compressor, flush
   * mode and a default buffer size.
   *
   * @param out the output stream
   * @param def the compressor ("deflater")
   * @param syncFlush if {@code true} the {@link #flush()} method of this instance flushes the
   * compressor with flush mode {@link Deflater#SYNC_FLUSH} before flushing the output stream,
   * otherwise only flushes the output stream
   * @since 1.7
   */
  public DeflaterOutputStream(OutputStream out,
      Deflater def,
      boolean syncFlush) {
    this(out, def, 512, syncFlush);
  }


  /**
   * Creates a new output stream with the specified compressor and
   * a default buffer size.
   *
   * <p>The new output stream instance is created as if by invoking
   * the 3-argument constructor DeflaterOutputStream(out, def, false).
   *
   * @param out the output stream
   * @param def the compressor ("deflater")
   */
  public DeflaterOutputStream(OutputStream out, Deflater def) {
    this(out, def, 512, false);
  }

  boolean usesDefaultDeflater = false;


  /**
   * Creates a new output stream with a default compressor, a default
   * buffer size and the specified flush mode.
   *
   * @param out the output stream
   * @param syncFlush if {@code true} the {@link #flush()} method of this instance flushes the
   * compressor with flush mode {@link Deflater#SYNC_FLUSH} before flushing the output stream,
   * otherwise only flushes the output stream
   * @since 1.7
   */
  public DeflaterOutputStream(OutputStream out, boolean syncFlush) {
    this(out, new Deflater(), 512, syncFlush);
    usesDefaultDeflater = true;
  }

  /**
   * Creates a new output stream with a default compressor and buffer size.
   *
   * <p>The new output stream instance is created as if by invoking
   * the 2-argument constructor DeflaterOutputStream(out, false).
   *
   * @param out the output stream
   */
  public DeflaterOutputStream(OutputStream out) {
    this(out, false);
    usesDefaultDeflater = true;
  }

  /**
   * Writes a byte to the compressed output stream. This method will
   * block until the byte can be written.
   *
   * @param b the byte to be written
   * @throws IOException if an I/O error has occurred
   */
  public void write(int b) throws IOException {
    byte[] buf = new byte[1];
    buf[0] = (byte) (b & 0xff);
    write(buf, 0, 1);
  }

  /**
   * Writes an array of bytes to the compressed output stream. This
   * method will block until all the bytes are written.
   *
   * @param b the data to be written
   * @param off the start offset of the data
   * @param len the length of the data
   * @throws IOException if an I/O error has occurred
   */
  public void write(byte[] b, int off, int len) throws IOException {
    if (def.finished()) {
      throw new IOException("write beyond end of stream");
    }
    if ((off | len | (off + len) | (b.length - (off + len))) < 0) {
      throw new IndexOutOfBoundsException();
    } else if (len == 0) {
      return;
    }
    if (!def.finished()) {
      def.setInput(b, off, len);
      while (!def.needsInput()) {
        deflate();
      }
    }
  }

  /**
   * Finishes writing compressed data to the output stream without closing
   * the underlying stream. Use this method when applying multiple filters
   * in succession to the same output stream.
   *
   * @throws IOException if an I/O error has occurred
   */
  public void finish() throws IOException {
    if (!def.finished()) {
      def.finish();
      while (!def.finished()) {
        deflate();
      }
    }
  }

  /**
   * Writes remaining compressed data to the output stream and closes the
   * underlying stream.
   *
   * @throws IOException if an I/O error has occurred
   */
  public void close() throws IOException {
    if (!closed) {
      finish();
      if (usesDefaultDeflater) {
        def.end();
      }
      out.close();
      closed = true;
    }
  }

  /**
   * Writes next block of compressed data to the output stream.
   *
   * @throws IOException if an I/O error has occurred
   */
  protected void deflate() throws IOException {
    int len = def.deflate(buf, 0, buf.length);
    if (len > 0) {
      out.write(buf, 0, len);
    }
  }

  /**
   * Flushes the compressed output stream.
   *
   * If {@link #DeflaterOutputStream(OutputStream, Deflater, int, boolean)
   * syncFlush} is {@code true} when this compressed output stream is
   * constructed, this method first flushes the underlying {@code compressor}
   * with the flush mode {@link Deflater#SYNC_FLUSH} to force
   * all pending data to be flushed out to the output stream and then
   * flushes the output stream. Otherwise this method only flushes the
   * output stream without flushing the {@code compressor}.
   *
   * @throws IOException if an I/O error has occurred
   * @since 1.7
   */
  public void flush() throws IOException {
    if (syncFlush && !def.finished()) {
      int len = 0;
      while ((len = def.deflate(buf, 0, buf.length, Deflater.SYNC_FLUSH)) > 0) {
        out.write(buf, 0, len);
        if (len < buf.length) {
          break;
        }
      }
    }
    out.flush();
  }
}
